ΦFlow Animation Gallery¶

GitHub   •   Documentation   •   API   •   Demos

Google Collab Book

This notebook shows various animations created with ΦFlow. To animate a plot, simply pass sequence data to vis.plot() and specify the time dimension using the animate argument.

All animations are rendered with Matplotlib and ffmpeg.

In [1]:
# !pip install --quiet phiflow
In [2]:
from phi.flow import *

Line Plots¶

We define the sine waves $\sin(x - t)$ and $\sin(x + t)$ and sample them on a grid from $x=0$ to $x=2\pi$ with resolution $R_x = 100$. This is done for 60 values of $t$, linearly spaced between $0$ and $4\pi$. These curves are animated in the left plot and their sum, a standing wave, is plotted on the right.

In [3]:
curves = CenteredGrid(lambda x, t: stack([math.sin(x - t), math.cos(x + t)], channel('c')), x=100, t=60, bounds=Box(x=2*PI, t=4*PI)).t[:-1]
plot({"Curves": curves, "Sum": sum(curves.c)}, animate='t')
Out[3]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Geometric Primitives¶

Geometric primitives like Sphere and Box can be plotted directly. Instance dimensions denote collections of objects.

In [4]:
plot(Sphere(x=wrap([0, 2], instance('s')), y=0, radius=math.linspace(0, 1, batch(t=50))**.5), animate='t')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In [4], line 1
----> 1 plot(Sphere(x=wrap([0, 2], instance('s')), y=0, radius=math.linspace(0, 1, batch(t=50))**.5), animate='t')

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:325, in plot(lib, row_dims, col_dims, animate, title, size, same_scale, show_color_bar, frame_time, repeat, *fields, **plt_args)
    323 else:
    324     min_val = max_val = None
--> 325 subplots = {pos: _space(fields, animate) for pos, fields in positioning.items()}
    326 if isinstance(title, str):
    327     title = {pos: title for pos in positioning}

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:325, in <dictcomp>(.0)
    323 else:
    324     min_val = max_val = None
--> 325 subplots = {pos: _space(fields, animate) for pos, fields in positioning.items()}
    326 if isinstance(title, str):
    327     title = {pos: title for pos in positioning}

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:426, in _space(fields, ignore_dims)
    424 all_dims = []
    425 for f in fields:
--> 426     for dim in f.bounds.vector.item_names:
    427         if dim not in all_dims and dim not in ignore_dims:
    428             all_dims.append(dim)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_point_cloud.py:112, in PointCloud.bounds(self)
    110 else:
    111     from phi.field._field_math import data_bounds
--> 112     bounds = data_bounds(self.elements.center)
    113     radius = math.max(self.elements.bounding_radius())
    114     return Box(bounds.lower - radius, bounds.upper + radius)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_field_math.py:264, in data_bounds(loc)
    262 min_vec = math.min(loc, dim=loc.shape.non_batch.non_channel)
    263 max_vec = math.max(loc, dim=loc.shape.non_batch.non_channel)
--> 264 return Box(min_vec, max_vec)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_box.py:196, in Box.__init__(self, lower, upper, **size)
    194     assert isinstance(lower, Tensor)
    195     assert 'vector' in lower.shape, "lower must have a vector dimension"
--> 196     assert lower.vector.item_names is not None, "vector dimension of lower must list spatial dimension order"
    197     self._lower = lower
    198 if upper is not None:

AssertionError: vector dimension of lower must list spatial dimension order
In [5]:
x = math.range(instance(boxes=10))
plot(Box(x=(x, x+1), y=(0, 2 * math.sin(math.linspace(0, 2*PI, batch(t=30)) + x*.5))), animate='t')
/tmp/ipykernel_1992/3754032892.py:2: DeprecationWarning: Creating a Box without item names prevents certain operations like project()
  plot(Box(x=(x, x+1), y=(0, 2 * math.sin(math.linspace(0, 2*PI, batch(t=30)) + x*.5))), animate='t')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In [5], line 2
      1 x = math.range(instance(boxes=10))
----> 2 plot(Box(x=(x, x+1), y=(0, 2 * math.sin(math.linspace(0, 2*PI, batch(t=30)) + x*.5))), animate='t')

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:325, in plot(lib, row_dims, col_dims, animate, title, size, same_scale, show_color_bar, frame_time, repeat, *fields, **plt_args)
    323 else:
    324     min_val = max_val = None
--> 325 subplots = {pos: _space(fields, animate) for pos, fields in positioning.items()}
    326 if isinstance(title, str):
    327     title = {pos: title for pos in positioning}

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:325, in <dictcomp>(.0)
    323 else:
    324     min_val = max_val = None
--> 325 subplots = {pos: _space(fields, animate) for pos, fields in positioning.items()}
    326 if isinstance(title, str):
    327     title = {pos: title for pos in positioning}

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:426, in _space(fields, ignore_dims)
    424 all_dims = []
    425 for f in fields:
--> 426     for dim in f.bounds.vector.item_names:
    427         if dim not in all_dims and dim not in ignore_dims:
    428             all_dims.append(dim)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_point_cloud.py:112, in PointCloud.bounds(self)
    110 else:
    111     from phi.field._field_math import data_bounds
--> 112     bounds = data_bounds(self.elements.center)
    113     radius = math.max(self.elements.bounding_radius())
    114     return Box(bounds.lower - radius, bounds.upper + radius)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_field_math.py:264, in data_bounds(loc)
    262 min_vec = math.min(loc, dim=loc.shape.non_batch.non_channel)
    263 max_vec = math.max(loc, dim=loc.shape.non_batch.non_channel)
--> 264 return Box(min_vec, max_vec)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_box.py:196, in Box.__init__(self, lower, upper, **size)
    194     assert isinstance(lower, Tensor)
    195     assert 'vector' in lower.shape, "lower must have a vector dimension"
--> 196     assert lower.vector.item_names is not None, "vector dimension of lower must list spatial dimension order"
    197     self._lower = lower
    198 if upper is not None:

AssertionError: vector dimension of lower must list spatial dimension order

Quiver Plots¶

In addition to the point locations, PointClouds can store per-point values, such as vectors.

In [6]:
x = math.rotate_vector(vec(x=1, y=0), angle=math.linspace(0, 2*PI, spatial(points=50)))
dx, x = x.points[1:] - x.points[:-1], x.points[:-1]
plot(vis.overlay(PointCloud(x, dx), rename_dims(PointCloud(x, dx, color='#40FFFF'), 'points', 'time')), animate='time')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In [6], line 3
      1 x = math.rotate_vector(vec(x=1, y=0), angle=math.linspace(0, 2*PI, spatial(points=50)))
      2 dx, x = x.points[1:] - x.points[:-1], x.points[:-1]
----> 3 plot(vis.overlay(PointCloud(x, dx), rename_dims(PointCloud(x, dx, color='#40FFFF'), 'points', 'time')), animate='time')

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_point_cloud.py:45, in PointCloud.__init__(self, elements, values, extrapolation, add_overlapping, bounds, color)
     29 def __init__(self,
     30              elements: Tensor or Geometry,
     31              values: Any = 1.,
   (...)
     34              bounds: Box = None,
     35              color: str or Tensor or tuple or list or None = None):
     36     """
     37     Args:
     38       elements: `Tensor` or `Geometry` object specifying the sample points and sizes
   (...)
     43       color: (optional) hex code for color or tensor of colors (same length as elements) in which points should get plotted.
     44     """
---> 45     SampledField.__init__(self, elements, math.wrap(values), extrapolation, bounds)
     46     self._add_overlapping = add_overlapping
     47     color = '#0060ff' if color is None else color

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_field.py:173, in SampledField.__init__(self, elements, values, extrapolation, bounds)
    166 """
    167 Args:
    168   elements: Geometry object specifying the sample points and sizes
    169   values: values corresponding to elements
    170   extrapolation: values outside elements
    171 """
    172 if isinstance(elements, Tensor):
--> 173     elements = Point(elements)
    174 assert isinstance(elements, Geometry), elements
    175 assert isinstance(values, Tensor), f"Values must be a Tensor but got {values}."

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_geom.py:454, in Point.__init__(self, location)
    452 def __init__(self, location: math.Tensor):
    453     assert 'vector' in location.shape, "location must have a vector dimension"
--> 454     assert location.shape.get_item_names('vector') is not None, "Vector dimension needs to list spatial dimension as item names."
    455     self._location = location

AssertionError: Vector dimension needs to list spatial dimension as item names.

2D Scalar Noise¶

Here we visualize the built-in class Noise, sampling it on a $64^3$ grid ranging from 0 to 10 along each axis. We plot all $x$-$y$ slices over time, yielding a scanning animation. The left plot shows noise with a smoothness of 1.0 and the right plot shows the same random noise (equal seed) with smoothness of 1.3.

In [7]:
noise = Noise(smoothness=stack({"Default Noise": 1.0, "Smooth Noise": 1.3}, batch('c')))
grid = CenteredGrid(noise, x=64, y=64, z=64, bounds=Box(x=10, y=10, z=10))
plot(grid, animate='z', show_color_bar=False)
Out[7]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Solar System¶

This animation shows two planets circling the sun, using a PointCloud with spherical elements for visualization.

In [8]:
PLANETS = instance(planets='Sun,Earth,Mars')
x = tensor([(0, 0), (9, 0), (0, 12)], PLANETS, channel(vector='x,y'))
x = math.rotate_vector(x, math.linspace(0, wrap([0, 5, 3], PLANETS), batch(time=130)))
plot(PointCloud(Sphere(x, radius=wrap([1, .4, .2], PLANETS))), animate='time')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/IPython/core/formatters.py:342, in BaseFormatter.__call__(self, obj)
    340     method = get_real_method(obj, self.print_method)
    341     if method is not None:
--> 342         return method()
    343     return None
    344 else:

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1338, in Animation._repr_html_(self)
   1336 fmt = mpl.rcParams['animation.html']
   1337 if fmt == 'html5':
-> 1338     return self.to_html5_video()
   1339 elif fmt == 'jshtml':
   1340     return self.to_jshtml()

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1266, in Animation.to_html5_video(self, embed_limit)
   1262 Writer = writers[mpl.rcParams['animation.writer']]
   1263 writer = Writer(codec='h264',
   1264                 bitrate=mpl.rcParams['animation.bitrate'],
   1265                 fps=1000. / self._interval)
-> 1266 self.save(str(path), writer=writer)
   1267 # Now open and base64 encode.
   1268 vid64 = base64.encodebytes(path.read_bytes())

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1068, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
   1063 with mpl.rc_context({'savefig.bbox': None}), \
   1064      writer.saving(self._fig, filename, dpi), \
   1065      cbook._setattr_cm(self._fig.canvas,
   1066                        _is_saving=True, manager=None):
   1067     for anim in all_anim:
-> 1068         anim._init_draw()  # Clear the initial frame
   1069     frame_number = 0
   1070     # TODO: Currently only FuncAnimation has a save_count
   1071     #       attribute. Can we generalize this to all Animations?

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1706, in FuncAnimation._init_draw(self)
   1698         warnings.warn(
   1699             "Can not start iterating the frames for the initial draw. "
   1700             "This can be caused by passing in a 0 length sequence "
   (...)
   1703             "it may be exhausted due to a previous display or save."
   1704         )
   1705         return
-> 1706     self._draw_frame(frame_data)
   1707 else:
   1708     self._drawn_artists = self._init_func()

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1728, in FuncAnimation._draw_frame(self, framedata)
   1724 self._save_seq = self._save_seq[-self.save_count:]
   1726 # Call the func with framedata and args. If blitting is desired,
   1727 # func needs to return a sequence of any artists that were modified.
-> 1728 self._drawn_artists = self._func(framedata, *self._args)
   1730 if self._blit:
   1732     err = RuntimeError('The animation function must return a sequence '
   1733                        'of Artist objects.')

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_matplotlib/_matplotlib_plots.py:99, in MatplotlibPlots.animate.<locals>.clear_and_plot(frame)
     96         axis.set_subplotspec(specs[axis])
     97         # axis.set_title(titles[axis])
     98 # plt.tight_layout()
---> 99 plot_frame_function(frame)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:339, in plot.<locals>.plot_frame(frame)
    337 for pos, fields in positioning.items():
    338     for f in fields:
--> 339         f = f[{animate.name: frame}]
    340         plots.plot(f, figure, axes[pos], subplots[pos], min_val=min_val, max_val=max_val, show_color_bar=show_color_bar, **plt_args)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_point_cloud.py:58, in PointCloud.__getitem__(self, item)
     56 if not item:
     57     return self
---> 58 elements = self.elements[{dim: selection for dim, selection in item.items() if dim != 'vector'}]
     59 values = self._values[item]
     60 color = self._color[item]

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_sphere.py:115, in Sphere.__getitem__(self, item)
    113 def __getitem__(self, item):
    114     item = slicing_dict(self, item)
--> 115     return Sphere(self._center[_keep_vector(item)], self._radius[item])

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_sphere.py:32, in Sphere.__init__(self, center, radius, **center_)
     30     assert isinstance(center, Tensor), "center must be a Tensor"
     31     assert 'vector' in center.shape, f"Sphere center must have a 'vector' dimension."
---> 32     assert center.shape.get_item_names('vector') is not None, f"Vector dimension must list spatial dimensions as item names. Use the syntax Sphere(x=x, y=y) to assign names."
     33     self._center = center
     34 else:

AssertionError: Vector dimension must list spatial dimensions as item names. Use the syntax Sphere(x=x, y=y) to assign names.
Out[8]:
<matplotlib.animation.FuncAnimation at 0x7f03884ef670>

3D Voxels¶

Two spheres are placed in a $32^3$ domain, at positions (16, 16, 0) and (16, 16, 32). Their radii grow linearly in time. These spheres are then sampled on a regular grid and plotted as voxels. Additionally, we plot the cross section $y=16$ as a 2D heat map.

In [9]:
sphere = Sphere(x=16, y=16, z=0, radius=math.linspace(0, 16, batch(time=17)))
grid = CenteredGrid(union(sphere, sphere.shifted((0, 0, 32))), x=32, y=32, z=32)
plot({"3D": grid, "2D Slice": grid.y[16]}, animate='time', frame_time=300)
Out[9]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Spirals¶

For these animated spirals, we plot 200 points whose distance increases linearly from the origin and whose angle increases linearly from 0 to $\alpha = 20 \frac{t}{T}$ where $t$ denotes the current frame and $T$ the number of frames. When no geometric shape is specified, PointClouds are plotted as x.

In [10]:
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(0, 20, batch(t=100)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/IPython/core/formatters.py:342, in BaseFormatter.__call__(self, obj)
    340     method = get_real_method(obj, self.print_method)
    341     if method is not None:
--> 342         return method()
    343     return None
    344 else:

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1338, in Animation._repr_html_(self)
   1336 fmt = mpl.rcParams['animation.html']
   1337 if fmt == 'html5':
-> 1338     return self.to_html5_video()
   1339 elif fmt == 'jshtml':
   1340     return self.to_jshtml()

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1266, in Animation.to_html5_video(self, embed_limit)
   1262 Writer = writers[mpl.rcParams['animation.writer']]
   1263 writer = Writer(codec='h264',
   1264                 bitrate=mpl.rcParams['animation.bitrate'],
   1265                 fps=1000. / self._interval)
-> 1266 self.save(str(path), writer=writer)
   1267 # Now open and base64 encode.
   1268 vid64 = base64.encodebytes(path.read_bytes())

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1068, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
   1063 with mpl.rc_context({'savefig.bbox': None}), \
   1064      writer.saving(self._fig, filename, dpi), \
   1065      cbook._setattr_cm(self._fig.canvas,
   1066                        _is_saving=True, manager=None):
   1067     for anim in all_anim:
-> 1068         anim._init_draw()  # Clear the initial frame
   1069     frame_number = 0
   1070     # TODO: Currently only FuncAnimation has a save_count
   1071     #       attribute. Can we generalize this to all Animations?

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1706, in FuncAnimation._init_draw(self)
   1698         warnings.warn(
   1699             "Can not start iterating the frames for the initial draw. "
   1700             "This can be caused by passing in a 0 length sequence "
   (...)
   1703             "it may be exhausted due to a previous display or save."
   1704         )
   1705         return
-> 1706     self._draw_frame(frame_data)
   1707 else:
   1708     self._drawn_artists = self._init_func()

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1728, in FuncAnimation._draw_frame(self, framedata)
   1724 self._save_seq = self._save_seq[-self.save_count:]
   1726 # Call the func with framedata and args. If blitting is desired,
   1727 # func needs to return a sequence of any artists that were modified.
-> 1728 self._drawn_artists = self._func(framedata, *self._args)
   1730 if self._blit:
   1732     err = RuntimeError('The animation function must return a sequence '
   1733                        'of Artist objects.')

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_matplotlib/_matplotlib_plots.py:99, in MatplotlibPlots.animate.<locals>.clear_and_plot(frame)
     96         axis.set_subplotspec(specs[axis])
     97         # axis.set_title(titles[axis])
     98 # plt.tight_layout()
---> 99 plot_frame_function(frame)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:339, in plot.<locals>.plot_frame(frame)
    337 for pos, fields in positioning.items():
    338     for f in fields:
--> 339         f = f[{animate.name: frame}]
    340         plots.plot(f, figure, axes[pos], subplots[pos], min_val=min_val, max_val=max_val, show_color_bar=show_color_bar, **plt_args)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_point_cloud.py:58, in PointCloud.__getitem__(self, item)
     56 if not item:
     57     return self
---> 58 elements = self.elements[{dim: selection for dim, selection in item.items() if dim != 'vector'}]
     59 values = self._values[item]
     60 color = self._color[item]

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_geom.py:510, in Point.__getitem__(self, item)
    509 def __getitem__(self, item):
--> 510     return Point(self._location[_keep_vector(slicing_dict(self, item))])

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_geom.py:454, in Point.__init__(self, location)
    452 def __init__(self, location: math.Tensor):
    453     assert 'vector' in location.shape, "location must have a vector dimension"
--> 454     assert location.shape.get_item_names('vector') is not None, "Vector dimension needs to list spatial dimension as item names."
    455     self._location = location

AssertionError: Vector dimension needs to list spatial dimension as item names.
Out[10]:
<matplotlib.animation.FuncAnimation at 0x7f03803c3fa0>

Varying the parameters can produce vastly different patterns.

In [11]:
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(PI*200, 1.1*PI*200, batch(t=200)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/IPython/core/formatters.py:342, in BaseFormatter.__call__(self, obj)
    340     method = get_real_method(obj, self.print_method)
    341     if method is not None:
--> 342         return method()
    343     return None
    344 else:

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1338, in Animation._repr_html_(self)
   1336 fmt = mpl.rcParams['animation.html']
   1337 if fmt == 'html5':
-> 1338     return self.to_html5_video()
   1339 elif fmt == 'jshtml':
   1340     return self.to_jshtml()

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1266, in Animation.to_html5_video(self, embed_limit)
   1262 Writer = writers[mpl.rcParams['animation.writer']]
   1263 writer = Writer(codec='h264',
   1264                 bitrate=mpl.rcParams['animation.bitrate'],
   1265                 fps=1000. / self._interval)
-> 1266 self.save(str(path), writer=writer)
   1267 # Now open and base64 encode.
   1268 vid64 = base64.encodebytes(path.read_bytes())

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1068, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
   1063 with mpl.rc_context({'savefig.bbox': None}), \
   1064      writer.saving(self._fig, filename, dpi), \
   1065      cbook._setattr_cm(self._fig.canvas,
   1066                        _is_saving=True, manager=None):
   1067     for anim in all_anim:
-> 1068         anim._init_draw()  # Clear the initial frame
   1069     frame_number = 0
   1070     # TODO: Currently only FuncAnimation has a save_count
   1071     #       attribute. Can we generalize this to all Animations?

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1706, in FuncAnimation._init_draw(self)
   1698         warnings.warn(
   1699             "Can not start iterating the frames for the initial draw. "
   1700             "This can be caused by passing in a 0 length sequence "
   (...)
   1703             "it may be exhausted due to a previous display or save."
   1704         )
   1705         return
-> 1706     self._draw_frame(frame_data)
   1707 else:
   1708     self._drawn_artists = self._init_func()

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/matplotlib/animation.py:1728, in FuncAnimation._draw_frame(self, framedata)
   1724 self._save_seq = self._save_seq[-self.save_count:]
   1726 # Call the func with framedata and args. If blitting is desired,
   1727 # func needs to return a sequence of any artists that were modified.
-> 1728 self._drawn_artists = self._func(framedata, *self._args)
   1730 if self._blit:
   1732     err = RuntimeError('The animation function must return a sequence '
   1733                        'of Artist objects.')

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_matplotlib/_matplotlib_plots.py:99, in MatplotlibPlots.animate.<locals>.clear_and_plot(frame)
     96         axis.set_subplotspec(specs[axis])
     97         # axis.set_title(titles[axis])
     98 # plt.tight_layout()
---> 99 plot_frame_function(frame)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/vis/_vis.py:339, in plot.<locals>.plot_frame(frame)
    337 for pos, fields in positioning.items():
    338     for f in fields:
--> 339         f = f[{animate.name: frame}]
    340         plots.plot(f, figure, axes[pos], subplots[pos], min_val=min_val, max_val=max_val, show_color_bar=show_color_bar, **plt_args)

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/field/_point_cloud.py:58, in PointCloud.__getitem__(self, item)
     56 if not item:
     57     return self
---> 58 elements = self.elements[{dim: selection for dim, selection in item.items() if dim != 'vector'}]
     59 values = self._values[item]
     60 color = self._color[item]

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_geom.py:510, in Point.__getitem__(self, item)
    509 def __getitem__(self, item):
--> 510     return Point(self._location[_keep_vector(slicing_dict(self, item))])

File /opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/geom/_geom.py:454, in Point.__init__(self, location)
    452 def __init__(self, location: math.Tensor):
    453     assert 'vector' in location.shape, "location must have a vector dimension"
--> 454     assert location.shape.get_item_names('vector') is not None, "Vector dimension needs to list spatial dimension as item names."
    455     self._location = location

AssertionError: Vector dimension needs to list spatial dimension as item names.
Out[11]:
<matplotlib.animation.FuncAnimation at 0x7f038037eb50>

Bouncing Balls¶

This demo visualizes the evolution of a PointCloud as a 3D scatter plot animation.

Thirty balls are placed at random locations. The initial velocities are sampled from a normal distribution with standard deviations $\sigma_x = \sigma_y = 1$ and $\sigma_z = 2$.

A simulation is than run for 100 frames, performing the following operations at each step:

  • Gravity is applied, $g_z = -9.81$,
  • Friction is computed proportional to velocity,
  • The balls are advected using Euler integration,
  • When below $z=0$, the y velocity is negated to simulate an elastic collision with the floor.
In [12]:
x0 = math.random_uniform(instance(balls=30), channel(vector='x,y,z')) + 5
balls = PointCloud(Sphere(x0, radius=.1), math.random_normal(x0.shape) * (1, 1, 2))
def step(balls, dt=.1):
  balls *= math.where(balls.points.vector['z'] < 0, (1, 1, -1), 1) * 0.7 ** dt
  return advect.points(balls, balls, dt) + (0, 0, -9.81 * dt)
plot(iterate(step, batch(t=100), balls).mask(), animate='t')
Out[12]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Burgers' Equation¶

Burgers' equation is a partial differential equation consisting of an advection term and a diffusion term acting on a vector field $v$ (velocity). It reads

$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x}.$$

Here, we simulate Burgers' equation on a $64^2$ grid for 100 time steps with $\Delta t = 0.5$, starting with a randomly generated initial condition. The evolution is plotted as a vector field. A standalone demo of Burgers' equation is also available here.

In [13]:
velocity = CenteredGrid(Noise(smoothness=1.5, vector='x,y'), extrapolation.PERIODIC, x=64, y=64) * 2
def burgers(v, dt=.5):
    return diffuse.explicit(advect.semi_lagrangian(v, v, dt), .08, dt)
vis.plot(iterate(burgers, batch(time=100), velocity), animate='time')
/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/phi/math/_shape.py:1525: RuntimeWarning: Stacking shapes with incompatible item names will result in item names being lost. Got ('x', 'y') and None
  warnings.warn(f"Stacking shapes with incompatible item names will result in item names being lost. Got {item_names[index]} and {items}", RuntimeWarning)
Out[13]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Incompressible Flow¶

Next, we simulate an incompressible fluid with moderate diffusion. We split the PDE

$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x} - \nabla p \quad \mathrm{s.t.} \quad \nabla \cdot v = 0$$

into advection, diffusion and pressure projection but will rely purely on numerical diffusion in this example. Starting from a random initial conditions, the fluid is simulated for 40 time steps and the vorticity $w = \nabla \times v$ and the pressure $p$ are shown. Also check out the tutorial notebook or the standalone Python scripts.

In [14]:
def incompressible_fluid_step(v: StaggeredGrid, p: CenteredGrid, dt=.5):
    return fluid.make_incompressible(advect.advect(v, v, dt), (), Solve('auto', 1e-5, 1e-5, x0=p))
trj = iterate(incompressible_fluid_step, batch(time=40), *fluid.make_incompressible(StaggeredGrid(Noise(), 0, x=64, y=64)))
plot({"Vorticity": field.curl(trj[0]), "Pressure": trj[1]}, animate='time', same_scale=False)
Out[14]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Pressure Solve¶

The incompressibility constraint $\nabla \cdot v$ in the Navier-Stokes equations is numerically achieved by solving the linear system of equations

$$\nabla p = \nabla \cdot v'$$

which yields the pseudo-pressure $p$. This is typically done with a conjugate gradient solver using a laplace stencil (5-point in 2D, 7-point in 3D). This demo visualizes how the pressure optimization progresses internally for a tentative velocity $v' = (1, 1)$ inside a circle at the center of the $100^2$ domain and $v' = 0$ outside.

In [15]:
with math.SolveTape(record_trajectories=True) as solves:
  fluid.make_incompressible(StaggeredGrid(Sphere(x=50, y=50, radius=20), 0, x=100, y=100))
plot(solves[0].x, animate='trajectory', frame_time=50)
Out[15]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Reaction-Diffusion¶

This simulation consists of two quantities $u$ and $v$ that interact via a non-linear partial differential equation (PDE) involving diffusion terms, $\nabla^2 u$ and $\nabla^2 v$. Depending on the exact form and parameters of the PDE, a myriad of resulting patterns can be achieved. The simulation is run for 1000 frames but we only plot every 10th since small time steps must be chosen for stability.

In [16]:
def reaction_diffusion(u, v, du=.19, dv=.05, f=.06, k=.062, dt=1.):
    return u + dt * du * field.laplace(u) - u * v**2 + f * (1 - u), v + dt * dv * field.laplace(v) + u * v**2 - (f + k) * v
trj_u, trj_v = iterate(reaction_diffusion, batch(time=1000), *[CenteredGrid(Noise(scale=20, smoothness=1.3), x=100, y=100) * .2 + .5]*2)
plot(trj_u.time[::10], animate='time')
Out[16]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>